﻿using System;
using System.Collections.Generic;
using CI.Interfaces;
using System.Threading;
using CI.Organism;

namespace CI.Mesopotamia
{
    /// <summary>
    /// This class handles the collision of entitys and determines if their is a winner and loser, IFitnessFunction
    /// </summary>
    public class CollisionDetection : FitnessFunction
    {
        /// <summary>Collection of colliding organisms</summary>
        private CollisionPairCollection _collisionPairs;
        private readonly SimEnv _simEnv = SimEnv.Instance;

        /// <summary>
        /// Proccess Collisions in the environment based on organisms cooridantes
        /// Step 1. Detect collisons
        /// Step 2. Apply Collison Energy Impact to both parties
        /// todo : this needs work as it becomes expotentinly slower the more organisms 
        /// </summary>
        /// <param name="organisms">Organisms checking for collisons</param>
        public override void UpdateOrganisms(IOrganismCollection organisms)
        {
            #region Argument Validators

            if (organisms == null)
                throw new ArgumentException("Organisms");
            #endregion

            // set timer as i suspect this is sucking up lot of cpu
#if DEBUG
            _simEnv.TraceLogger.LogDebug("Running Collision Checking");
            var swTotal = new System.Diagnostics.Stopwatch();
            var swDetectCollisions = new System.Diagnostics.Stopwatch();
            swTotal.Start();
#endif

            // update the locations of everyone, add the dirty ones to the list then clear their dirty status
            organisms.DirtyLocationsRebuild();

            // stats
            _simEnv.TraceLogger.LogDebug(string.Format("Active Org Count : {0}", organisms.Alive.Count + organisms.Dead.Count));
            _simEnv.TraceLogger.LogDebug(string.Format("Dirty Locations : {0}", organisms.DirtyLocations.Count));

            // run boundry clipping only on dirty locations
            // the theory is if something has been created or been moved it will be marked dirty and this is much quicker 
            // just checking 100 or so items then possibly all 10 thousand or so objects
            // save the results back into dirty locations
            organisms.DirtyLocations = new List<IOrganism>(BoundryClipping(organisms.DirtyLocations));

            // check for collisions if any of them have moved
            if (organisms.DirtyLocations.Count > 0)
            {
                // detect collisions
#if DEBUG
                swDetectCollisions.Start();
#endif
                _collisionPairs = DetectCollisionsPairs(organisms);
#if DEBUG
                swDetectCollisions.Stop();
#endif


                // dump count to screen
                _simEnv.TraceLogger.LogDebug(string.Format("Collisions detected : {0}", _collisionPairs.Count));

                // calculate the result
                CalculateCollisionEnergyResult(_collisionPairs);
            }

            // how long did it take
#if DEBUG
            swTotal.Stop();
            _simEnv.TraceLogger.LogDebug(string.Format("Collision Detection Checking - Elapsed Milliseconds : {0}", swDetectCollisions.ElapsedMilliseconds));
            _simEnv.TraceLogger.LogDebug(string.Format("Finishing Collision Checking - Elapsed Milliseconds : {0}", swTotal.ElapsedMilliseconds));
#endif
        }


        /// <summary>
        /// Detect collisions in the collection and create a new collection with the colliding organisms
        /// Note: this is currently only using object Boundary calc by (pos + org radias), i intend to add better 
        /// checking.
        /// </summary>
        /// <param name="organisms">All the organims to be tested</param>
        /// <returns>a list of collision pair collection</returns>
        private CollisionPairCollection DetectCollisionsPairs(IOrganismCollection organisms)
        {
            #region Argument Validators

            if (organisms == null)
                throw new ArgumentException("Organisms");
            #endregion

            // resulting collection
            var collisionParColletionResult = new CollisionPairCollection();
            // copy them into a list to reduce threading lock issues
            var origOrganisms = new List<IOrganism>(organisms);
            var dirtyOrganisms = new List<IOrganism>(organisms.DirtyLocations);

            _simEnv.TraceLogger.LogDebug(string.Format("Detect Collisions Pairs - Dirty : {0} Total Compare Organisms : {1}", dirtyOrganisms.Count, origOrganisms.Count));
            _simEnv.TraceLogger.LogDebug(string.Format("Null checkOrganisms : {0}", dirtyOrganisms.Contains(null)));

            // cycle through each of the organisms to see if any of them are colliding
            Parallel.ForEach(dirtyOrganisms, (checkOrganism, psDirty) =>
            {
                // make sure it exists in the environment
                if (!(checkOrganism.Born))
                {
                    return;
                }

                // save the location for the childOrganism
                ILocation checkOrganismLocation = checkOrganism.Location;

                // compare it against the other organisms
                foreach (Organism.Organism organism in origOrganisms)
                {
                    // if they are the same CIET then no point comparing
                    if (checkOrganism.CIET == organism.CIET)
                        continue;

                    // make sure we are not comparing against ourself, if so continue
                    if (checkOrganism == organism)
                        continue;

                    // make sure it exists in the environment
                    if (!(organism.Born))
                        continue;

                    // if their locations collide
                    if (!LocationCollisions.LocationsCollide(checkOrganismLocation, organism.Location))
                        continue;


                    // if they got all this way then they collided so add them to the collection
                    // first detect and make sure their not in their already
                    if (Monitor.TryEnter(collisionParColletionResult, Globals.CriticalAcquireLockTimeout))
                    {
                        try
                        {
                            if (!(collisionParColletionResult.Contains(new CollisionPair(checkOrganism, organism))))
                                collisionParColletionResult.Add(new CollisionPair(checkOrganism, organism));
                        }
                        finally
                        {
                            Monitor.Exit(collisionParColletionResult);
                        }
                    }
                    else
                    {
                        throw new CollisionDetectionException("Unable to acquire lock on _collisionParColletionResult");
                    }
                }
            });

            // return result
            return collisionParColletionResult;
        }




        /// <summary>
        /// Takes a collection of colliding organisms pairs and updates their energy status
        /// This consists of adjusting each organisms energy level by the others Collision Impact Energy Transfer
        /// So if org1 has a CIET of 10 we take 10 of org2 and add it to org1, the one with the highest CIET is the one 
        /// that we use to transfer the energy, eg it only goes one way to the highest...
        /// 
        /// This method will also set ActiveState to ignore once its body mass energy is delepted and wont be renedered 
        /// on the screen, so it has to delete entitys and move it from the deleted list to the inactice
        /// </summary>
        /// <param name="collisionPairCollection">Collection of Pairs of colliding organisms to calculate the energy impact</param>
        private void CalculateCollisionEnergyResult(IEnumerable<CollisionPair> collisionPairCollection)
        {
            #region Argument Validators

            if (!(collisionPairCollection is CollisionPairCollection))
                throw new ArgumentException("collisionPairCollection");
            #endregion

            // organisms
            IOrganism winner;
            IOrganism losser;

            foreach (CollisionPair collisionPair in collisionPairCollection)
            {
                // if their equal CIET then do next collision pair
                if (collisionPair.Organism1.CIET == collisionPair.Organism2.CIET)
                    continue;

                // calculate the winning energy
                if (collisionPair.Organism1.CIET > collisionPair.Organism2.CIET)
                {
                    winner = collisionPair.Organism1;
                    losser = collisionPair.Organism2;
                }
                else
                {
                    losser = collisionPair.Organism1;
                    winner = collisionPair.Organism2;
                }

                // subtract the energy result if energy is still positive and give it to the winner
                losser.Energy -= winner.CIET;
                winner.Energy += winner.CIET;

                // if energy is negative take it off the body mass index and reset energy to 0
                // give the energy to the winner
                if (losser.Energy < 0)
                {
                    losser.BMEnergy += losser.Energy;
                    losser.Energy = 0;
                }

                // if body mass index is negative its long dead
                if (losser.BMEnergy < 0)
                {
                    // check first to make sure its in the dead list
                    if (losser.Organisms.Dead.Contains(losser))
                    {
                        if (Monitor.TryEnter(losser.Organisms.Dead, Globals.CriticalAcquireLockTimeout))
                        {
                            try
                            {
                                if (Monitor.TryEnter(losser.Organisms.Inactive, Globals.CriticalAcquireLockTimeout))
                                {
                                    try
                                    {
                                        // swtich lists
                                        losser.Organisms.DeadRemove(losser);
                                        losser.Organisms.InactiveAdd(losser);

                                        // set it to inactive
                                        losser.ActiveState = ActiveState.Ignore;

                                        // remove it from the hosting environment
                                        losser.HostingEnvironment.RegisterPhysicalCell(losser.Cells.FindPhysicalZygote(), ActionType.RemoveCell);
                                    }
                                    finally
                                    {
                                        Monitor.Exit(losser.Organisms.Inactive);
                                    }
                                }
                                else
                                {
                                    throw new CollisionDetectionException("Unable to acquire lock on _losser.Organisms.Inactive");
                                }
                            }
                            finally
                            {
                                Monitor.Exit(losser.Organisms.Dead);
                            }
                        }
                        else
                        {
                            throw new CollisionDetectionException("Unable to acquire lock on _losser.Organisms.Dead");
                        }
                    }
                }
            }

        }


        /// <summary>
        /// Checks all the organisms in the environment to make sure their withing the hosting boundry
        /// </summary>
        public  IList<IOrganism> BoundryClipping(IList<IOrganism> organisms)
        {
            #region Argument Validators
            if (organisms == null)
                throw new ArgumentException("organisms");
            #endregion

            // number or organisms clipped from environment
            Int64 clippedCount = 0;
            var results = new List<IOrganism>(organisms);

            _simEnv.TraceLogger.LogDebug(String.Format("Starting Boundry Clipping checking of {0} items", organisms.Count));

            // go through and check each one of them
            Parallel.ForEach(new List<IOrganism>(organisms), (organism, psDirty) =>
            //foreach (IOrganism organism in new List<IOrganism>(organisms))
            {
                if (organism.HostingEnvironment.BoundsChecking(organism))
                {
                    clippedCount++;
                    if (results.Contains(organism))
                        results.Remove(organism);
                }
            });

            _simEnv.TraceLogger.LogDebug(string.Format("Ending Boundry Clipping, Clipped {0} organisms", clippedCount));

            return results;
        }

        /// <summary>
        /// Collision Detection Exceptions
        /// </summary>
        private class CollisionDetectionException : Exception
        {
            /// <summary>
            /// Collision Detection exception
            /// </summary>
            /// <param name="message"></param>
            public CollisionDetectionException(string message) : base(message) { }
        }
    }

    /// <summary>Collection of collision pairs</summary>
    public class CollisionPairCollection : List<CollisionPair>
    {
    }
}
